<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Spark • Multiple Splats</title>
  <link rel="stylesheet" href="style.css">
</head>

<body>
  <a href="#" id="mobile_button">Menu</a>
  <div id="menu">
    <div class="border">
      <div class="border">
        <h3>Tipatat's Splat Restaurant</h3>
        <h1> Menu </h1>
        <h2>CHEF SPECIALS</h2>
        <div id="menu_list"></div>
        <h2>Food scans by <a href="https://x.com/tipatat" target="_blank">Tipatat</a></h2>
      </div>
    </div>
  </div>
  <script type="importmap">
    {
      "imports": {
        "three": "/examples/js/vendor/three/build/three.module.js",
        "three/addons/": "/examples/js/vendor/three/examples/jsm/",
        "@sparkjsdev/spark": "/dist/spark.module.js"
      }
    }
    </script>
  <script type="module">
    import * as THREE from "three";
    import { SplatMesh } from "@sparkjsdev/spark";
    import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
    import { EXRLoader } from "three/addons/loaders/EXRLoader.js";
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    import { FOOD_ASSETS, FOOD_URL } from './food.js';
    import { getAssetFileURL } from "/examples/js/get-asset-url.js";
    import { preloadSplats } from "/examples/js/preloader.js";

    // Add food items to the menu
    FOOD_ASSETS.forEach((food, i) => {
      const el = document.createElement("a");
      el.textContent = food.name;
      el.href = 'javascript:;';
      el.addEventListener('click', async function () {
        switchToFood(i);
      });
      document.getElementById('menu_list').appendChild(el);
    });

    // Setup mobile menu button
    let IS_MOBILE = 'ontouchstart' in window || navigator.msMaxTouchPoints;
    document.getElementById('mobile_button').addEventListener('click', () => {
      document.getElementById('menu').classList.toggle('visible');
    });

    const scene = new THREE.Scene();
    const renderer = new THREE.WebGLRenderer();
    renderer.shadowMap.enabled = true;
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // Setup camera
    const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 0.9, -1.2);

    // handle windows resize
    window.addEventListener('resize', onWindowResize, false);
    function onWindowResize() {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    }

    // Setup mouse controls to orbit the camera around
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0.2, 0, 0);
    controls.minDistance = 0.8;
    controls.maxDistance = 2.3;
    controls.enablePan = false;
    controls.update();


    // Current and next food splats
    let food, nextFood;
    // Other items
    let table, shadow;

    // Add table
    const gltfLoader = new GLTFLoader();
    const modelURL = await getAssetFileURL("table.glb");
    const gltfTable = await gltfLoader.loadAsync(modelURL);
    table = gltfTable.scene;

    // Transition length in frames.
    // Two transitions: one for fading out the old food, another for fading in the next one
    const TRANSITION_LENGTH = IS_MOBILE ? 30 : 60;
    // Transition timers. `null` if transition is not active
    let fadeOutTime = null;
    let fadeInTime = null;

    // Preload all splat files
    let splats;
    preloadSplats(FOOD_ASSETS.map(t => t.file)).then(loaded_splats => {
      splats = loaded_splats;
      init();
    });

    async function init() {
      // Setup lighting
      const spotLight = new THREE.SpotLight(0xffcc88);
      spotLight.position.set(0, 1, 0);
      spotLight.castShadow = true;
      spotLight.shadow.mapSize.width = 1024;
      spotLight.shadow.mapSize.height = 1024;
      spotLight.shadow.camera.near = 0.1;
      spotLight.shadow.camera.far = 5;
      spotLight.angle = 0.9;
      spotLight.penumbra = 1;
      spotLight.intensity = 3;
      scene.add(spotLight);

      const fillLight = new THREE.PointLight(0xffcc88, 0.2);
      fillLight.position.set(0, 0, -3);
      scene.add(fillLight);

      // Splats don't project shadows, so we add a cylinder below the spotlight to fake one ;)
      const geometry = new THREE.CylinderGeometry(0.45, 0.45, 0.04, 40, 1);
      const material = new THREE.MeshPhongMaterial({ colorWrite: false, depthWrite: false });
      shadow = new THREE.Mesh(geometry, material);
      shadow.visible = false;
      shadow.castShadow = true;
      shadow.position.set(0, 0.1, 0);
      scene.add(shadow);

      // Set the table cloth to receive shadows
      const tableCloth = table.children.find(item => item.name == 'cover');
      tableCloth.receiveShadow = true;
      scene.add(table);

      // Add floor
      const plane = new THREE.PlaneGeometry(10, 10);
      const floormat = new THREE.MeshPhongMaterial({ color: 0x777777 });
      const floor = new THREE.Mesh(plane, floormat);
      floor.rotation.x = -Math.PI / 2;
      floor.position.set(0, -1.397, 0);
      scene.add(floor);

      // Show menu
      document.getElementById('menu').classList.add('visible');
      if (IS_MOBILE) document.getElementById('mobile_button').classList.add('visible');

      // Load first food by default
      switchToFood(0);

      // Start render loop
      renderer.setAnimationLoop(animate);
    }

    function animate(time) {
      controls.update();
      renderer.render(scene, camera);
      const rotation = time / 10000;
      table.rotation.y = rotation;
      if (food) food.rotation.y = -rotation;
      if (nextFood) nextFood.rotation.y = -rotation;


      // fade out
      if (fadeOutTime !== null) {
        fadeOutTime++;
        if (fadeOutTime < TRANSITION_LENGTH) {
          if (food) food.opacity = 1 - easeInOutSine(fadeOutTime / TRANSITION_LENGTH);
        } else {
          // Fade out finished
          if (food) food.dispose();
          fadeOutTime = null;
          // Fade in next food
          fadeInTime = 0;
          shadow.visible = true;
          shadow.scale.setScalar(shadow.nextScale);
        }
      }

      // fade in
      if (fadeInTime != null && nextFood.isInitialized) {
        fadeInTime++;
        if (fadeInTime < TRANSITION_LENGTH) {
          nextFood.opacity = easeInOutSine(fadeInTime / TRANSITION_LENGTH);
        } else {

          // Fade in finished
          food = nextFood;
          fadeInTime = null;
        }
      }
    };

    // Change food from menu link
    function switchToFood(foodIndex) {

      // already transitioning
      if (fadeOutTime !== null || fadeInTime !== null) return;

      const foodItem = FOOD_ASSETS[foodIndex];

      nextFood = splats[foodItem.file];
      nextFood.quaternion.set(1, 0, 0, 0);

      // Customize splat depending on the settings set for this food
      if (foodItem['offsetY']) nextFood.position.set(0, foodItem.offsetY, 0);
      nextFood.scale.setScalar(foodItem.scale);
      shadow.nextScale = foodItem.shadowSize;

      // Set opacity to 0 to prepare the fade in transition
      nextFood.opacity = 0;

      // Setup shadow to the required size, and make it visible only when the splat is initialized
      nextFood.initialized.then(() => {
        shadow.visible = false;
        fadeOutTime = 0; // Fade in next food
      });

      scene.add(nextFood);

      // Hide menu if on mobile
      if (IS_MOBILE) document.getElementById('menu').classList.remove('visible');

      // toggle menu item
      const menu_items = document.getElementById('menu_list').children;
      for (let i = 0; i < menu_items.length; i++) {
        menu_items[i].classList.toggle('active', foodIndex == i);
      }
    }

    function easeInOutSine(x) {
      return -(Math.cos(Math.PI * x) - 1) / 2;
    }

  </script>
</body>

</html>
